﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Shlook.Common;
using System.ServiceModel;
using System.Threading;

namespace Shlook.Server
{
    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single, IncludeExceptionDetailInFaults=true)]
    public class ChatService : IChatServer
    {
        private Logger Log { get { return Logger.Default; } }

        private class UserInfo
        {
            public UserInfo(User user)
            {
                User = user;
                RebuildChannel();
                ErrorCount = 0;
            }
            
            public User User { get; set; }
            public IChatClient Channel { get; set; }
            public int ErrorCount { get; private set;  }

            public void RebuildChannel()
            {
                ++ErrorCount;
                Channel = ChannelFactory<IChatClient>.CreateChannel(Bindings.Default, new EndpointAddress(User.URL));
            }
        }

        private ServiceHost _host;
        private Dictionary<string, UserInfo> _connectedUsers = new Dictionary<string, UserInfo>();

        private void PerformForUser(UserInfo user, Action<UserInfo> action)
        {
            try
            {
                action(user);
            }
            catch (CommunicationException)
            {
                Log.Log("An error occurred when dispatching a message to " + user.User.DisplayName);
                RebuildChannelAsync(user);
            }
            catch (TimeoutException)
            {
                Log.Log("An error occurred when dispatching a message to " + user.User.DisplayName);
                RebuildChannelAsync(user);
            }
        }

        private const int ErrorCountThreshold = 1;

        private void RebuildChannelAsync(UserInfo user)
        {
            ThreadPool.QueueUserWorkItem(delegate
            {
                if (user.ErrorCount > ErrorCountThreshold)
                {
                    Log.Log("Failed to build channel to user " + user.User.DisplayName + ", disconnecting");
                    Disconnect(user.User.Id);
                }
                else
                {
                    user.RebuildChannel();
                }
            });
        }

        private void PerformForUsers(Predicate<UserInfo> predicate, Action<UserInfo> action)
        {
            UserInfo[] users;
            lock (_connectedUsers)
            {
                users = _connectedUsers.Values.Where(u => predicate(u)).ToArray();
                //Note: we're forcing query execution here because otherwise we
                //enumerate the collection outside the lock, introducing a data race.
            }
            foreach (UserInfo user in users)
            {
                PerformForUser(user, action);
            }
        }

        private Timer _keepAlive;
        private const int KeepAliveInterval = 10000;

        private void KeepAlive()
        {
            PerformForUsers(u => true, u => u.Channel.KeepAlive());
        }

        public void Open(string listenAddress)
        {
            _host = new ServiceHost(this);
            _host.AddServiceEndpoint(typeof(IChatServer), Bindings.Default, listenAddress);
            _host.Open();

            Log.Log("Host is open; server is listening");

            _keepAlive = new Timer((o) => KeepAlive());
            _keepAlive.Change(0, KeepAliveInterval);
        }

        public void Close()
        {
            _host.Close(TimeSpan.FromSeconds(1));

            Log.Log("Host is closed; server is down");
        }

        public void Connect(User user)
        {
            Log.Log("User " + user.DisplayName + " (userid = " + user.Id + ", from " + user.URL + ")");

            UserInfo info = new UserInfo(user);
            lock (_connectedUsers)
            {
                if (_connectedUsers.ContainsKey(user.Id))
                {
                    _connectedUsers[user.Id] = info;
                }
                else
                {
                    _connectedUsers.Add(user.Id, info);
                }
            }

            PerformForUsers(u => u.User.Id != user.Id, u => u.Channel.UserConnected(user));
        }

        public void ChangeStatus(string userId, string newStatus)
        {
            UserInfo user;
            lock (_connectedUsers)
            {
                if (!_connectedUsers.TryGetValue(userId, out user))
                {
                    throw ChatFault.Create("The specified user is no longer connected");
                }

                user.User.Status = newStatus;
            }

            Log.Log("User " + userId + " changed status to " + newStatus);

            PerformForUsers(u => u.User.Id != userId, u => u.Channel.UserStatusChanged(user.User));
        }

        public void Disconnect(string userId)
        {
            Log.Log("User " + userId + " disconnected");

            lock (_connectedUsers)
            {
                _connectedUsers.Remove(userId);
            }

            PerformForUsers(u => u.User.Id != userId, u => u.Channel.UserDisconnected(userId));
        }

        public User[] GetUsers()
        {
            lock (_connectedUsers)
            {
                return (from u in _connectedUsers.Values select u.User).ToArray();
            }
        }

        public void SendMessage(ChatMessage[] messages)
        {
            foreach (ChatMessage message in messages)
            {
                UserInfo toUser, fromUser;
                lock (_connectedUsers)
                {
                    if (!_connectedUsers.TryGetValue(message.ToUser, out toUser))
                    {
                        throw ChatFault.Create("Target user is no longer connected");
                    }
                    if (!_connectedUsers.TryGetValue(message.FromUser, out fromUser))
                    {
                        throw ChatFault.Create("Source user is no longer connected");
                    }
                }

                PerformForUser(toUser, u => u.Channel.NewMessage(fromUser.User, message.Message));

                Log.Log("User " + message.FromUser + " sent message to " + message.ToUser);
            }
        }
    }
}
